﻿using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.IO;
using System.Linq;
using System.Threading;
using Lucene.Linq.Search;
using Lucene.Linq.Utility;

namespace Lucene.Linq.Storage.EntityFramework
{
    /// <summary>Index Set for an LINQ to EF DataContext
    /// TODO: DC instance rebuilding see http://www.codeplex.com/linqtolucene/WorkItem/View.aspx?WorkItemId=6072
    /// </summary>
    /// <typeparam name="TDataContext">The DataContext type</typeparam>
    public class EfDatabaseIndexSet<TDataContext> : IndexSet
        where TDataContext : DbContext
    {
        #region Fields/Properties

        readonly ReaderWriterLockSlim _dataContextLock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);
        TDataContext _dataContext;


        /// <summary>
        /// The data context instance. 
        /// Data Contexts should be recycled periodically based on unit-of-work basis.
        /// 
        /// </summary>
        public TDataContext DataContext
        {
            get { return _dataContext; }
            set
            {
                if (value == null)
                    throw new ArgumentNullException("value");

                using (_dataContextLock.WriteLock())
                {
                    _dataContext = value;
                }
            }
        }

        #endregion

        #region Ctors/Init

        /// <summary>Creates RAM Indexes from data context</summary>
        /// <param name="dataContext">The data context instance</param>
        public EfDatabaseIndexSet(TDataContext dataContext)
            : base()
        {
            if (dataContext == null)
                throw new ArgumentNullException("dataContext");

            DataContext = dataContext;
            Init();
        }

        ///<summary>Creates File System Indexes from data context</summary>
        ///<param name="path">File System path</param>
        /// <param name="dataContext">The data context instance</param>
        public EfDatabaseIndexSet(string path, TDataContext dataContext)
            : base(path)
        {
            if (dataContext == null)
                throw new ArgumentNullException("dataContext");

            DataContext = dataContext;
            Init();
        }

        ///<summary>Creates file system indexes from data context</summary>
        ///<param name="directory">File system directory info</param>
        /// <param name="dataContext">The data context instance.</param>
        public EfDatabaseIndexSet(DirectoryInfo directory, TDataContext dataContext)
            : base(directory)
        {
            if (dataContext == null)
                throw new ArgumentNullException("dataContext");

            DataContext = dataContext;
            Init();
        }

        public EfDatabaseIndexSet(Net.Store.Directory directory, TDataContext dataContext)
            : base(directory)
        {
            if (dataContext == null)
                throw new ArgumentNullException("dataContext");

            DataContext = dataContext;
            Init();
        }


        private void Init()
        {
            // get all tables in db context
            var tableTypes = GetTableTypes();

            // Throw if there are no tables
            if (!tableTypes.Any())
            {
                throw new ArgumentException("The data context type has no Tables");
            }

            // if there are any tables on the context, add them to the set
            foreach (var tableType in tableTypes)
            {
                try
                {
                    Add(tableType);
                }
                catch (ArgumentException)
                {
                    // linq table type doesn't have Document attribute
                    // for now, skipping this table type makes sense
                }
            }

        }

        #endregion

        #region Private Methods

        private IEnumerable<Type> GetTableTypes()
        {
            //get all our properties that derive from dbset
            //and has 1 type argument, our table
            //then get the generic argument which is the table type and populate the indexes
            return _dataContext.GetType().GetProperties().Where(p => p.PropertyType.Name == typeof(DbSet<>).Name
                                                                    && p.PropertyType.GetGenericArguments().Count() == 1)
                                                                    .Select(property => property.PropertyType.GetGenericArguments()[0]);
        }


        #endregion

        #region Public Methods

        /// <summary>Write all the records from the table type into their respective indexes</summary>
        /// <typeparam name="TTable">Table type to index</typeparam>
        /// <exception cref="ArgumentException">The type provided isn't a valid table within the db context</exception>
        public void Write<TTable>() where TTable : class , new()
        {
            Write(typeof(TTable));
        }

        /// <summary>
        /// Writes all the records from the provided table type into thier respective indexes
        /// </summary>
        /// <param name="tableType">The type of table</param>
        /// <exception cref="ArgumentException">The type provided isn't a valid table within the db context</exception>
        public void Write(Type tableType)
        {
            using (_dataContextLock.ReadLock())
            {
                IIndex index = Get(tableType);
                string name = tableType.Name;
                var tableData = _dataContext.Set(tableType);

                if (tableData == null)
                    throw new ArgumentException("tableType doesnt belong to db");

                // get all the records from the table and insert into the index
                index.Add(tableData);
                Console.WriteLine("Added " + index.Count + " " + name + "s.");
            }
        }

        /// <summary>Write all the records from each table into their respective indexes</summary>
        public void Write()
        {
            using (_dataContextLock.ReadLock())
            {

                foreach (var tableType in GetTableTypes())
                {
                    try
                    {
                        Write(tableType);
                    }
                    catch (ArgumentException)
                    {

                        // skip this table type
                        Console.WriteLine("Skipping Type: " + tableType.FullName);
                    }
                }
            }
        }

        #endregion
    }
}
